/*******************************************************************************
 * Copyright (c) 2000, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package com.architexa.org.eclipse.gef.editpolicies;

import com.architexa.org.eclipse.draw2d.AbsoluteBendpoint;
import com.architexa.org.eclipse.draw2d.AutomaticRouter;
import com.architexa.org.eclipse.draw2d.Bendpoint;
import com.architexa.org.eclipse.draw2d.Connection;
import com.architexa.org.eclipse.draw2d.geometry.Point;
import com.architexa.org.eclipse.draw2d.geometry.PointList;
import com.architexa.org.eclipse.draw2d.geometry.Rectangle;
import com.architexa.org.eclipse.gef.ConnectionEditPart;
import com.architexa.org.eclipse.gef.EditPart;
import com.architexa.org.eclipse.gef.Request;
import com.architexa.org.eclipse.gef.commands.Command;
import com.architexa.org.eclipse.gef.handles.BendpointCreationHandle;
import com.architexa.org.eclipse.gef.handles.BendpointMoveHandle;
import com.architexa.org.eclipse.gef.requests.BendpointRequest;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;



/**
 * Used to add bendpoint handles on a {@link ConnectionEditPart}.
 * <P>
 * BendpointEditPolicy will automatically observe the {@link
 * com.architexa.org.eclipse.draw2d.Connection} figure. If the number of bends in the
 * <code>Connection</code> changes, the handles will be updated.
 */
public abstract class BendpointEditPolicy
	extends SelectionHandlesEditPolicy
	implements PropertyChangeListener
{

private static final List NULL_CONSTRAINT = new ArrayList();

private List originalConstraint;
private boolean isDeleting = false;

private static final Point ref1 = new Point();
private static final Point ref2 = new Point();

/**
 * <code>activate()</code> is extended to add a listener to the <code>Connection</code>
 * figure.
 * @see com.architexa.org.eclipse.gef.EditPolicy#activate()
 */
public void activate() {
	super.activate();
	getConnection().addPropertyChangeListener(Connection.PROPERTY_POINTS, this);
}

private List createHandlesForAutomaticBendpoints() {
	List list = new ArrayList();
	ConnectionEditPart connEP = (ConnectionEditPart)getHost();
	PointList points = getConnection().getPoints();
	for (int i = 0; i < points.size() - 1; i++)
		list.add(new BendpointCreationHandle(connEP, 0, i));
	
	return list;
}

private List createHandlesForUserBendpoints() {
	List list = new ArrayList();
	ConnectionEditPart connEP = (ConnectionEditPart)getHost();
	PointList points = getConnection().getPoints();
	List bendPoints = (List)getConnection().getRoutingConstraint();
	int bendPointIndex = 0;
	Point currBendPoint = null;
	
	if (bendPoints == null)
		bendPoints = NULL_CONSTRAINT;
	else if (!bendPoints.isEmpty())
		currBendPoint = ((Bendpoint)bendPoints.get(0)).getLocation();
	
	for (int i = 0; i < points.size() - 1; i++) {
		//Put a create handle on the middle of every segment
		list.add(new BendpointCreationHandle(connEP, bendPointIndex, i));
		
		//If the current user bendpoint matches a bend location, show a move handle
		if (i < points.size() - 1
				&& bendPointIndex < bendPoints.size()
				&& currBendPoint.equals(points.getPoint(i + 1))) {
			list.add(new BendpointMoveHandle(connEP, bendPointIndex, i + 1));
			
			//Go to the next user bendpoint
			bendPointIndex++;
			if (bendPointIndex < bendPoints.size())
				currBendPoint = ((Bendpoint)bendPoints.get(bendPointIndex)).getLocation();
		}
	}

	return list;
}

/**
 * Creates selection handles for the bendpoints.  Explicit (user-defined) bendpoints will
 * have {@link BendpointMoveHandle}s on them with a single  {@link
 * BendpointCreationHandle} between 2 consecutive explicit bendpoints. If implicit
 * bendpoints (such as those created by the {@link AutomaticRouter}) are used, one {@link
 * BendpointCreationHandle} is placed in the middle of the Connection.
 * @see SelectionHandlesEditPolicy#createSelectionHandles()
 */
protected List createSelectionHandles() {
	List list = new ArrayList();
	if (isAutomaticallyBending())
		list = createHandlesForAutomaticBendpoints();
	else
		list = createHandlesForUserBendpoints();
 	return list;
}

/**
 * <code>deactivate()</code> is extended to remove the property change listener on the
 * <code>Connection</code> figure.
 * @see com.architexa.org.eclipse.gef.EditPolicy#deactivate()
 */
public void deactivate() {
	getConnection().removePropertyChangeListener(Connection.PROPERTY_POINTS, this);
	super.deactivate();
}

/**
 * Erases all bendpoint feedback. Since the original <code>Connection</code> figure is
 * used for feedback, we just restore the original constraint that was saved before
 * feedback started to show.
 * @param request the BendpointRequest
 */
protected void eraseConnectionFeedback(BendpointRequest request) {
	restoreOriginalConstraint();
	originalConstraint = null;
}

/**
 * @see com.architexa.org.eclipse.gef.EditPolicy#eraseSourceFeedback(Request)
 */
public void eraseSourceFeedback(Request request) {
	if (REQ_MOVE_BENDPOINT.equals(request.getType())
	  || REQ_CREATE_BENDPOINT.equals(request.getType()))
		eraseConnectionFeedback((BendpointRequest) request);
}

/**
 * Factors the Request into either a MOVE, a DELETE, or a CREATE of a bendpoint.
 * @see com.architexa.org.eclipse.gef.EditPolicy#getCommand(Request)
 */
public Command getCommand(Request request) {
	if (REQ_MOVE_BENDPOINT.equals(request.getType())) {
		if (isDeleting)
			return getDeleteBendpointCommand((BendpointRequest)request);
		return getMoveBendpointCommand((BendpointRequest)request);
	}
	if (REQ_CREATE_BENDPOINT.equals(request.getType()))
		return getCreateBendpointCommand((BendpointRequest)request);
	return null;
}

/**
 * Convenience method for obtaining the host's <code>Connection</code> figure.
 * @return the Connection figure
 */
protected Connection getConnection() {
	return (Connection)((ConnectionEditPart)getHost()).getFigure();
}

/**
 * Implement this method to return a Command that will create a bendpoint.
 * @param request the BendpointRequest
 * @return a Command to create a bendpoint
 */
protected abstract Command getCreateBendpointCommand(BendpointRequest request);

/**
 * Implement this method to return a Command that will delete a bendpoint.
 * @param request the BendpointRequest
 * @return a Command to delete a bendpoint
 */
protected abstract Command getDeleteBendpointCommand(BendpointRequest request);

/**
 * Implement this method to return a Command that will move a bendpoint.
 * @param request the BendpointRequest
 * @return a Command to move a bendpoint
 */
protected abstract Command getMoveBendpointCommand(BendpointRequest request);

private boolean isAutomaticallyBending() {
	List constraint = (List)getConnection().getRoutingConstraint();
	PointList points = getConnection().getPoints();
	return ((points.size() > 2) && (constraint == null || constraint.isEmpty()));
}

private boolean lineContainsPoint(Point p1, Point p2, Point p) {
	int tolerance = 7;
	Rectangle rect = Rectangle.SINGLETON;
	rect.setSize(0, 0);
	rect.setLocation(p1.x, p1.y);
	rect.union(p2.x, p2.y);
	rect.expand(tolerance, tolerance);
	if (!rect.contains(p.x, p.y))
		return false;

	int v1x, v1y, v2x, v2y;
	int numerator, denominator;
	double result = 0.0;

	if (p1.x != p2.x && p1.y != p2.y) {
		
		v1x = p2.x - p1.x;
		v1y = p2.y - p1.y;
		v2x = p.x - p1.x;
		v2y = p.y - p1.y;
		
		numerator = v2x * v1y - v1x * v2y;
		denominator = v1x * v1x + v1y * v1y;

		result = ((numerator << 10) / denominator * numerator) >> 10;
	}
	
	// if it is the same point, and it passes the bounding box test,
	// the result is always true.
	return result <= tolerance * tolerance;
}

/**
 * If the number of bendpoints changes, handles are updated.
 * @see java.beans.PropertyChangeListener#propertyChange(PropertyChangeEvent)
 */
public void propertyChange(PropertyChangeEvent evt) {
	//$TODO optimize so that handles aren't added constantly.
	if (getHost().getSelected() != EditPart.SELECTED_NONE)
		addSelectionHandles();	
}

/**
 * Restores the original constraint that was saved before feedback began to show.
 */
protected void restoreOriginalConstraint() {
	if (originalConstraint != null) {
		if (originalConstraint == NULL_CONSTRAINT)
			getConnection().setRoutingConstraint(null);
		else
			getConnection().setRoutingConstraint(originalConstraint);
	}
}

/**
 * Since the original figure is used for feedback, this method saves the 
 * original constraint, so that is can be restored when the feedback is
 * erased.
 */
protected void saveOriginalConstraint() {
	originalConstraint = (List)getConnection().getRoutingConstraint();
	if (originalConstraint == null)
		originalConstraint = NULL_CONSTRAINT;
	getConnection().setRoutingConstraint(new ArrayList(originalConstraint));
}

private void setReferencePoints(BendpointRequest request) {
	PointList points = getConnection().getPoints();
	int bpIndex = -1;
	List bendPoints = (List)getConnection().getRoutingConstraint();
	Point bp = ((Bendpoint)bendPoints.get(request.getIndex())).getLocation();
	
	int smallestDistance = -1;
	
	for (int i = 0; i < points.size(); i++) {
		if (smallestDistance == -1
				|| points.getPoint(i).getDistance2(bp) < smallestDistance) {
			bpIndex = i;
			smallestDistance = points.getPoint(i).getDistance2(bp);
			if (smallestDistance == 0)
				break;
		}
	}
	
	points.getPoint(ref1, bpIndex - 1);
	getConnection().translateToAbsolute(ref1);
	points.getPoint(ref2, bpIndex + 1);
	getConnection().translateToAbsolute(ref2);
}

/**
 * Shows feedback when a bendpoint is being created.  The original figure is used for
 * feedback and the original constraint is saved, so that it can be restored when feedback
 * is erased.
 * @param request the BendpointRequest
 */
protected void showCreateBendpointFeedback(BendpointRequest request) {
	Point p = new Point(request.getLocation());
	List constraint;
	getConnection().translateToRelative(p);
	Bendpoint bp = new AbsoluteBendpoint(p);
	if (originalConstraint == null) {
		saveOriginalConstraint();
		constraint = (List)getConnection().getRoutingConstraint();
		constraint.add(request.getIndex(), bp);
	} else {
		constraint = (List)getConnection().getRoutingConstraint();
	}
	constraint.set(request.getIndex(), bp);
	getConnection().setRoutingConstraint(constraint);
}

/**
 * Shows feedback when a bendpoint is being deleted.  This method is only called once when
 * the bendpoint is first deleted, not every mouse move.  The original figure is used for
 * feedback and the original  constraint is saved, so that it can be restored when
 * feedback is erased.
 * @param request the BendpointRequest
 */
protected void showDeleteBendpointFeedback(BendpointRequest request) {
	if (originalConstraint == null) {
		saveOriginalConstraint();
		List constraint = (List)getConnection().getRoutingConstraint();
		constraint.remove(request.getIndex());
		getConnection().setRoutingConstraint(constraint);
	}
}

/**
 * Shows feedback when a bendpoint is being moved.  Also checks to see if the bendpoint 
 * should be deleted and then calls {@link #showDeleteBendpointFeedback(BendpointRequest)}
 * if needed.  The original figure is used for feedback and the original constraint is 
 * saved, so that it can be restored when feedback is erased.
 * @param request the BendpointRequest
 */
protected void showMoveBendpointFeedback(BendpointRequest request) {
	Point p = new Point(request.getLocation());
	if (!isDeleting)
		setReferencePoints(request);
	
	if (lineContainsPoint(ref1, ref2, p)) {
		if (!isDeleting) {
			isDeleting = true;
			eraseSourceFeedback(request);
			showDeleteBendpointFeedback(request);
		}
		return;
	}
	if (isDeleting) {
		isDeleting = false;
		eraseSourceFeedback(request);
	}
	if (originalConstraint == null)
		saveOriginalConstraint();
	List constraint = (List)getConnection().getRoutingConstraint();
	getConnection().translateToRelative(p);
	Bendpoint bp = new AbsoluteBendpoint(p);
	constraint.set(request.getIndex(), bp);
	getConnection().setRoutingConstraint(constraint);
}

/**
 * Shows feedback when appropriate. Calls a different method depending on the request
 * type.
 * 
 * @see #showCreateBendpointFeedback(BendpointRequest)
 * @see #showMoveBendpointFeedback(BendpointRequest)
 * @param request the Request
 */
public void showSourceFeedback(Request request) {
	if (REQ_MOVE_BENDPOINT.equals(request.getType()))
		showMoveBendpointFeedback((BendpointRequest)request);
	else if (REQ_CREATE_BENDPOINT.equals(request.getType()))
		showCreateBendpointFeedback((BendpointRequest)request);
}

}
